/*
 * Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
 * All rights reserved.
 *
 * This file is part of Harmattan SmashMiner OpenGL game application.
 * 
 * Harmattan SmashMiner OpenGL Example Application version 1.0.0
 *  
 * Latest update: 15.4.2011
 *
 * The Harmattan SmashMiner OpenGL example application demonstrates how to use 
 * the OpenGL ES in Harmattan devices.
 *
 * This example is provided as a starting point for 3rd party
 * developers (you) to ease the implementation of OpenGL based 
 * games. Ideas, parts of code or methodologies that this
 * example application uses can be freely used without restrictions.
 *
 * See file README.txt how to build and compile this example application
 * in the Harmattan SDK environment. 
 * 
 * See file INSTALL.txt to find out what is required to run this
 * application and how to install the application to the device or
 * alternatively in QEMU-emulator.
 *
 * The LICENSE.txt file explains the license this software is provided
 * with. A copy of the same license is included in this source file.
 * 
 */

/*
 * Copyright (C) 2011 by Nokia Corporation.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "gameengine.h"
#include "gameobject.h"

#include <math.h>
#include <stdlib.h>
#include <QDebug>

/*! \class GameObject
 * Base class for graphical game objects.
 * When extending from GameObject usually some enumerations and constants are needed to create,
 * e.g. see the \c Missile constructor implementation:
 * \code
 * Missile::Missile(int bornFrame, bool rewindEnabled, FPtype guidanceFactor, FPtype speedFactor)
 *     : GameObject(TypeMissile, bornFrame, GameEngine::GameObjectRadiusMissile, SpeedLimitExtraHigh,
 *                  rewindEnabled, MassTiny, ScoreMissile) { ... }
 * \endcode
 *
 * - Create a new entry in the \c ::GameObjectType enumeration.
 * - Define a constant radius value for game object as in \c GameEngine::GameObjectRadiusMissile
 * - Select one of the speed limit values \c ::SpeedLimit for enemy object 
 * - Select one of the mass values in \c ::GameObjectMass
 * - Create a new entry in the \c ::ScoreValue
 */

///
int GameObject::collisionGroupCounter = 0;

/*!
  Constructor for GameObject.

  \a gameObjectType Type of the game object as ::GameObjectType

  \a bornFrame Current frame number. When the GameObject was created.

  \a gameObjectRadius Radius of the game object as ::FPtype.

  \a gameObjectSpeedLimit Maximum speed of the game object as ::SpeedLimit.

  \a rewindEnabled Is the rewind mode enabled.

  \a mass Mass of the game object as ::GameObjectMass.

  \a scoreValue Score value of the game object as ::ScoreValue.
*/
GameObject::GameObject(GameObjectType gameObjectType, int bornFrame,
                       FPtype gameObjectRadius, SpeedLimit gameObjectSpeedLimit, 
                       bool rewindEnabled, GameObjectMass mass, ScoreValue scoreValue) 
    : storedDiedFrame(-1)
    , storedBornFrame(bornFrame)
    , storedDiedTime(999.9) // is still alive...
    , type(gameObjectType)
    , radius(gameObjectRadius)
    , speedLimit(gameObjectSpeedLimit * 1.0f)
    , positionX(0.0)
    , positionY(0.0)
    , velocityX(0.0)
    , velocityY(0.0)
    , objectMass(mass)
    , score(scoreValue)
    , collisionGroup(-1)
#ifdef SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
    , buffer(NULL)
    , writeIndex(0)
    , hasWrapped(0)
    , readIndex(0)
#endif  // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
{

#ifdef SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE

    if (rewindEnabled)
        buffer = new QVector<frameData>(SMASHMINER_CONFIG_REWIND_BUFFER_CAPACITY);

#endif  // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE

}

/*!
Destructs GameObject.
*/
GameObject::~GameObject()
{
#ifdef SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE

    if (buffer != NULL) {
        delete buffer;
        buffer = NULL;
    }

#endif  // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
}

#ifdef SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE

void GameObject::setRewindEnabled(bool rewindEnabled)
{
    if (buffer != NULL) {
        delete buffer;
        buffer = NULL;
    }

    if (rewindEnabled)
        buffer = new QVector<frameData>(SMASHMINER_CONFIG_REWIND_BUFFER_CAPACITY);
}

#endif  // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE

/*!
  Check that the speed of the object does not go above GameObject speed limit.
*/
void GameObject::checkSpeed()
{
    if (speedLimit < 0.1) {
        qWarning() << "GameObject::checkSpeed() : speedlimit is too small.";
        return;
    }

    const FPtype totvel = sqrt(velocityX * velocityX + velocityY * velocityY);
    if (totvel > speedLimit) {
        const FPtype scaling = speedLimit / totvel;
        velocityX *= scaling;
        velocityY *= scaling;
    }
}

/*!
  Sets new speed to a GameObject.
*/
void GameObject::scaleSpeed(FPtype speed)
{
    if (speed > speedLimit) {
        speed = speedLimit;
    }

    const FPtype totvel = sqrt(velocityX * velocityX + velocityY * velocityY);
    const FPtype scaling = speed / totvel;
    velocityX *= scaling;
    velocityY *= scaling;
}

#ifdef SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE

/*!
    Is rewind mode enabled.
*/
bool GameObject::rewindEnabled()
{
    if (buffer != NULL)
        return true;
    else
        return false;
}

/*!
  Is GameObject alive at \a frame given.
*/
bool GameObject::isAliveAtFrame(int frame) const
{
    if (frame < storedBornFrame)
        return false;

    if (storedDiedFrame < 0)
        return true;

    if (frame > storedDiedFrame)
        return false;
    else
        return true;
}

/*!
   Writes object position to the rewind buffer for a given \a frame.
*/
void GameObject::writePosition(int frame)
{
    if (!rewindEnabled())
        qFatal("GameObject::writePosition() : rewind feature is disabled.");

#ifdef SMASHMINER_CONFIG_ENABLE_DEBUG_CHECKS

    // check that frame == (previous_frame + 1).
    //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    if (writeIndex != 0 || hasWrapped) {
        int prevIndex = writeIndex - 1;
        if (prevIndex < 0) prevIndex += SMASHMINER_CONFIG_REWIND_BUFFER_CAPACITY;

        if ((*buffer)[prevIndex].frameId + 1 != frame)
            qFatal("GameObject::writePosition() : gap in frame numbering. "
                "writeIndex= %d hasWrapped= %d objectType= %d", writeIndex, hasWrapped, objectType());
    }

#endif  // SMASHMINER_CONFIG_ENABLE_DEBUG_CHECKS

    (*buffer)[writeIndex].frameId = frame;
    (*buffer)[writeIndex].positionX = positionX;
    (*buffer)[writeIndex].positionY = positionY;

    if (++writeIndex == SMASHMINER_CONFIG_REWIND_BUFFER_CAPACITY) {
        writeIndex = 0;
        hasWrapped = true;
    }
}

/*!
    Initializes rewind mode starting from \a firstFrame.

    Determines into readIndex a best possible guess for a location where relevant data reading
    might start from, when the rewind mode is started from the 
    given frame.

    If \c firstFrame is older than \c bornFrame of the game object 
    the \c readIndex is set to \c bornFrame.

    \param firstFrame The rewind mode playback start frame.
*/
void GameObject::initRewindMode(int firstFrame)
{
    if (!rewindEnabled())
        qFatal("GameObject::initRewindMode() : rewind feature is disabled.");

    // determine into readIndex a best possible guess
    // for a location where relevant data reading might
    // start from, when the rewind mode is started from
    // frame firstFrame.

    // NOTICE that in our buffer the firstFrame record
    // does not even exist if bornFrame > firstFrame!!!
    // in this case set readIndex point to bornFrame.

    if (storedBornFrame > firstFrame)
        firstFrame = storedBornFrame;

    int searchEnd = SMASHMINER_CONFIG_REWIND_BUFFER_CAPACITY;

    if (!hasWrapped)
        searchEnd = writeIndex;

    readIndex = -1;

    for (int i = 0;i < searchEnd;i++) {
        if ((*buffer)[i].frameId == firstFrame) {
            readIndex = i;
            break;
        }
    }

    if (readIndex < 0) {

        bool framesSkipped = false;
        qCritical() << "Here are the valid frames for this object:";
        for (int i = 0;i < searchEnd;i++) {
            if (i > 10 && i < (searchEnd - 10)) {
                if (!framesSkipped)
                    qCritical() << "...skipping frames...";
                framesSkipped = true;
            }
            else {
                qCritical() << "index=" << i << " frameID=" << (*buffer)[i].frameId;
            }
        }

        qFatal("GameObject::initRewindMode() : frame %d not found, objectType = %d "
            "Perhaps the value SMASHMINER_CONFIG_REWIND_BUFFER_CAPACITY is too small"
            "compared to SMASHMINER_CONFIG_REWIND_PERIOD_SECONDS (see config.h).", 
            firstFrame, objectType());
    }
}

/*!
    Reads object position from a given \a frame to a \a pos from the rewind buffer.
*/
void GameObject::readPosition(int frame, GLfloat * pos)
{
    if (!rewindEnabled()) 
        qFatal("GameObject::readPosition() : rewind feature is disabled.");

    for (readIndex = 0;readIndex < SMASHMINER_CONFIG_REWIND_BUFFER_CAPACITY;readIndex++) {
        if ((*buffer)[readIndex].frameId == frame) {
            pos[0] = (*buffer)[readIndex].positionX;
            pos[1] = (*buffer)[readIndex].positionY;
            return;
        }
    }

    // in this case the search failed.

    pos[0] = 0.0;
    pos[1] = 0.0;
}

#endif  // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE

/*!
  Sets group of given GameObjects uncollidable for each other for certain amount of frames.
  Objects are set uncollidable for amount of \c collisionFreeFrames.
  Used for example when large ateroid is smahed for three smaller.
*/
void GameObject::setUnCollidable(QList<GameObject *> & objects)
{
    const int groupID = collisionGroupCounter++;

    for (int i = 0;i < objects.count();i++)
        objects.at(i)->collisionGroup = groupID;
}

/*!
    Checks whether GameObject is collidable with \a other GameObject.
    Objects are not collidable if they are in the same collision group and are still in collision free frames.
*/
bool GameObject::isCollidable(int currentFrame, GameObject *other) const
{
    if (collisionGroup < 0 || other->collisionGroup < 0)
        return true;

    if (currentFrame - storedBornFrame < collisionFreeFrames 
        && collisionGroup == other->collisionGroup) {
        return false;
    } else {
        return true;
    }
}

/*!
  Check whether GameObject is collidable on \a currentFrame.
*/
bool GameObject::isCollidable(int currentFrame) const
{
    return (currentFrame - storedBornFrame >= collisionFreeFrames);
}

/*! \class WrappingGameObject
  Base class for GameObjects that are wrapped around the screen.
*/

/*!
  Constructor for WrappingGameObject.

  \a gameObjectType Type of the game object as ::GameObjectType

  \a bornFrame Current number. When GameObject was born.

  \a gameObjectRadius Radius of the game object as ::FPtype.

  \a gameObjectSpeedLimit Maximum speed of the game object as ::SpeedLimit.

  \a rewindEnabled Is the rewind mode enabled.

  \a mass Mass of the game object as ::GameObjectMass.

  \a scoreValue Score value of the game object as ::ScoreValue.
*/
WrappingGameObject::WrappingGameObject(GameObjectType gameObjectType, int bornFrame,
                                       FPtype gameObjectRadius, SpeedLimit gameObjectSpeedLimit, 
                                       bool rewindEnabled, GameObjectMass mass, 
                                       ScoreValue scoreValue) 
    : GameObject(gameObjectType, bornFrame, gameObjectRadius, gameObjectSpeedLimit, rewindEnabled,
                 mass, scoreValue)
{
    storedWrappingFlags = 0;
    setWrappedPosX(posX());
    setWrappedPosX(posY());

    wrappingDisabledFlag = false;
}

/*!
    Test if there is need to draw the object again, "wrapped" to the opposite side of the screen.
    This is used in the normal game mode!
    The wrapping results are stored into WrappingGameObject to be used when drawing object to screen.
*/
bool WrappingGameObject::checkWrapping(FPtype viewRangeX, FPtype viewRangeY)
{
    if (wrappingDisabledFlag) {

        // check if we are already at screen,
        // and exit wrappingDisabled mode if yes.

        const FPtype rangeX = viewRangeX - objectRadius();
        const FPtype rangeY = viewRangeY - objectRadius();

        bool xOk = false;
        if (posX() < 0.0f) {
            if (posX() > -rangeX)
                xOk = true;
        }
        else {
            if (posX() < +rangeX)
                xOk = true;
        }

        bool yOk = false;
        if (posY() < 0.0f) {
            if (posY() > -rangeY)
                yOk = true;
        }
        else {
            if (posY() < +rangeY)
                yOk = true;
        }

        if (xOk && yOk)
            wrappingDisabledFlag = false;
    }

    bool wrapped = false;
    if (!wrappingDisabledFlag) {
        if (posX() > +viewRangeX) {
            setPosX(posX() - (viewRangeX * 2.0f));
            wrapped = true;
        }

        if (posX() < -viewRangeX) {
            setPosX(posX() + (viewRangeX * 2.0f));
            wrapped = true;
        }

        if (posY() > +viewRangeY) {
            setPosY(posY() - (viewRangeY * 2.0f));
            wrapped = true;
        }

        if (posY() < -viewRangeY) {
            setPosY(posY() + (viewRangeY * 2.0f));
            wrapped = true;
        }
    }

    bool bad_position = false;

    FPtype marginal = 1.0f;
    if (wrappingDisabledFlag)
        marginal = 1.5f;

    if (posX() < -viewRangeX * marginal)
        bad_position = true;

    if (posX() > +viewRangeX * marginal)
        bad_position = true;

    if (posY() < -viewRangeY * marginal)
        bad_position = true;

    if (posY() > +viewRangeY * marginal)
        bad_position = true;

    if (bad_position)
        qFatal("GameEngine::checkWrapping() : bad_position!!!"
                "objectType= %d wrappingDisabledFlag= %d", objectType(), wrappingDisabledFlag);

    if (wrappingDisabledFlag) {
        storedWrappingFlags = 0;
        return wrapped;
    }

    storedWrappingFlags = 0;
    setWrappedPosX(posX());
    setWrappedPosX(posY());

    if (-viewRangeX - posX() > -objectRadius()) {
        setWrappedPosX(posX() + viewRangeX * 2.0f);
        storedWrappingFlags |= WRAPPING_FLAG_WRAP_X;
    }

    if (+viewRangeX - posX() < +objectRadius()) {
        setWrappedPosX(posX() - viewRangeX * 2.0f);
        storedWrappingFlags |= WRAPPING_FLAG_WRAP_X;
    }

    if (-viewRangeY - posY() > -objectRadius()) {
        setWrappedPosY(posY() + viewRangeY * 2.0f);
        storedWrappingFlags |= WRAPPING_FLAG_WRAP_Y;
    }

    if (+viewRangeY - posY() < +objectRadius()) {
        setWrappedPosY(posY() - viewRangeY * 2.0f);
        storedWrappingFlags |= WRAPPING_FLAG_WRAP_Y;
    }

    return wrapped;
}

/*!
  Test if there is need to draw the object again, "wrapped" to the opposite side of the screen.
  This is used in the game rewind mode only!
  At rewind mode wrap checking differs from normal game mode:
  - There is no need to check the ranges.
  - the position information is not used from WrappingGameObject but an external source instead (the stored object locations).
  - position of the WrappingGameObject is not modified, but the wrapping information is.
*/
void WrappingGameObject::checkWrappingRewind(GLfloat *pos,FPtype viewRangeX, FPtype viewRangeY)
{
    storedWrappingFlags = 0;

    // the wrappingDisabled() flag only works during
    // the game mode. here we need to check the coordinates,
    // and if they are off-screen then assume we are at
    // wrappingDisabled mode.

    if (pos[0] < -viewRangeX || pos[0] > +viewRangeX) return;
    if (pos[1] < -viewRangeY || pos[1] > +viewRangeY) return;

    setWrappedPosX(pos[0]);
    setWrappedPosY(pos[1]);

    if (-viewRangeX - pos[0] > -objectRadius()) {
        setWrappedPosY(pos[0] + viewRangeX * 2.0f);
        storedWrappingFlags |= WRAPPING_FLAG_WRAP_X;
    }

    if (+viewRangeX - pos[0] < +objectRadius()) {
        setWrappedPosX(pos[0] - viewRangeX * 2.0f);
        storedWrappingFlags |= WRAPPING_FLAG_WRAP_X;
    }

    if (-viewRangeY - pos[1] > -objectRadius()) {
        setWrappedPosY(pos[1] + viewRangeY * 2.0f);
        storedWrappingFlags |= WRAPPING_FLAG_WRAP_Y;
    }

    if (+viewRangeY - pos[1] < +objectRadius()) {
        setWrappedPosY(pos[1] - viewRangeY * 2.0f);
        storedWrappingFlags |= WRAPPING_FLAG_WRAP_Y;
    }
}
